// ValidationAspects
// Written by Mike Saunders
// Released under Microsoft Public License (Ms-PL)
using System;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using ValidationAspects;
using ValidationAspects.Core;
using ValidationAspects.Exceptions;

namespace TestValidationAspects.Core
{
	[TestFixture]
	public class TestTypeValidatorFactory
	{
		[Test]
		public void Test_ValidateAction_executes_methods_declaring_ValidationMethodAttribute()
		{
			Assert.IsFalse(Validatable.Validate1WasCalled);
			Assert.IsFalse(Validatable.Validate2WasCalled);
			Assert.IsFalse(Validatable.Validate3WasCalled);

			TypeValidatorFactory<Validatable> typeValidatorFactory = new TypeValidatorFactory<Validatable>();
			typeValidatorFactory.Create<Validatable>().Invoke(new Validatable(), new MockValidationContext());

			Assert.IsTrue(Validatable.Validate1WasCalled);
			Assert.IsFalse(Validatable.Validate2WasCalled);
			Assert.IsTrue(Validatable.Validate3WasCalled);
		}

		[Test]
		public void Test_ValidateAction_HasValidationMethods_is_false_for_no_ValidationMethods()
		{
			TypeValidatorFactory<NoValidationMethods> typeValidatorFactory = new TypeValidatorFactory<NoValidationMethods>();
			Assert.IsFalse(typeValidatorFactory.HasValidationMethods);
		}

		[Test]
		public void Test_ValidateAction_HasValidationMethods_is_true_for_ValidationMethods()
		{
			TypeValidatorFactory<Validatable> typeValidatorFactory = new TypeValidatorFactory<Validatable>();
			Assert.IsTrue(typeValidatorFactory.HasValidationMethods);
		}

		[Test]
		public void Test_ValidateAction_throws_if_method_has_incorrect_return_type()
		{
			Test.Throws<ValidationException>(() => new TypeValidatorFactory<IncorrectReturnType>().Create<IncorrectReturnType>(),
			                                 string.Format("Method [name:{0}] signature must be [void Method({1})] if declaring attribute [type{2}].", "Validate", typeof(IncorrectReturnType),
			                                               typeof(ValidationMethodAttribute)));
		}

		[Test]
		public void Test_ValidateAction_throws_if_method_has_incorrect_parameter_count()
		{
			Test.Throws<ValidationException>(() => new TypeValidatorFactory<IncorrectParameterCount>().Create<IncorrectParameterCount>(),
			                                 string.Format("Method [name:{0}] signature must be [void Method({1})] if declaring attribute [type{2}].", "Validate", typeof(IncorrectParameterCount),
			                                               typeof(ValidationMethodAttribute)));
		}

		[Test]
		public void Test_ValidateAction_throws_if_method_has_incorrect_parameter_type()
		{
			Test.Throws<ValidationException>(() => new TypeValidatorFactory<IncorrectParameterType>().Create<IncorrectParameterType>(),
			                                 string.Format("Method [name:{0}] signature must be [void Method({1})] if declaring attribute [type{2}].", "Validate", typeof(IncorrectParameterType),
			                                               typeof(ValidationMethodAttribute)));
		}

		[Test]
		public void Test_ValidateAction_forwards_ValidationException_populated_exception()
		{
			ValidatableThrows value = new ValidatableThrows();
			IValidationContext context = new MockValidationContext();

			try
			{
				new TypeValidatorFactory<ValidatableThrows>().Create<ValidatableThrows>().Invoke(value, context);
			}
			catch (ValidationContextException e)
			{
				e.Context.TestValues(value, typeof(ValidatableThrows), null, null, null, null);

				ValidationException[] exceptions = e.Exceptions.Cast<ValidationException>().ToArray();
				object[] methodArgs = new[] {value};

				ValidationException validationException = exceptions[0];
				Assert.AreEqual("Validate1 failed", validationException.Message);
				validationException.Context.TestValues(value, typeof(ValidatableThrows), null, typeof(ValidatableThrows).GetMethod("Validate1"), null, methodArgs);

				validationException = exceptions[1];
				Assert.AreEqual("Validate2 failed", validationException.Message);
				validationException.Context.TestValues(value, typeof(ValidatableThrows), null, typeof(ValidatableThrows).GetMethod("Validate2"), null, methodArgs);

				return;
			}
			Assert.Fail("ValidationContextException not thrown.");
		}

		[Test]
		[ExpectedException(typeof(TargetInvocationException))]
		public void Test_ValidateAction_forwards_TargetInvocationException_if_inner_is_not_a_ValidationException()
		{
			new TypeValidatorFactory<ValidatableThrowsArgumentException>().Create<ValidatableThrowsArgumentException>().Invoke(new ValidatableThrowsArgumentException(), new MockValidationContext());
		}

		#region Mocks
		public class Validatable
		{
			public static bool Validate1WasCalled { get; set; }
			public static bool Validate2WasCalled { get; set; }
			public static bool Validate3WasCalled { get; set; }

			[ValidationMethod]
			public static void Validate1(Validatable instance)
			{
				Validate1WasCalled = true;
			}

			public static void Validate2(Validatable instance)
			{
				Validate2WasCalled = true;
			}

			[ValidationMethod]
			public static void Validate3(Validatable instance)
			{
				Validate3WasCalled = true;
			}

			public static bool Validate4(string s)
			{
				return true;
			}
		}

		public class NoValidationMethods {}

		public class IncorrectReturnType
		{
			[ValidationMethod]
			public static bool Validate(IncorrectReturnType instance)
			{
				return true;
			}
		}

		public class IncorrectParameterCount
		{
			[ValidationMethod]
			public static void Validate(IncorrectParameterCount instance, string s) {}
		}

		public class IncorrectParameterType
		{
			[ValidationMethod]
			public static void Validate(string instance) {}
		}

		public class ValidatableThrows
		{
			[ValidationMethod]
			public static void Validate1(ValidatableThrows instance)
			{
				throw new ValidationException("Validate1 failed");
			}

			[ValidationMethod]
			public static void Validate2(ValidatableThrows instance)
			{
				throw new ValidationException("Validate2 failed");
			}
		}

		public class ValidatableThrowsArgumentException
		{
			[ValidationMethod]
			public static void Validate(ValidatableThrowsArgumentException instance)
			{
				throw new ArgumentException("validation failed");
			}
		}
		#endregion
	}
}